#ifndef __SHM_HPP__
#define __SHM_HPP__

#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int gdefault_size = 4096; // 共享内存的大小最好是4096 的整数倍
std::string gpathname = "../shared_memory";
int gproj_id = 0x77;

class SharedMemory
{
private:
    bool ShmHelper(int flags)
    {
        // 利用函数ftok 形成键值,需要传文件路径以项目id;ftok 成功返回键值，失败返回-1
        _key = ftok(gpathname.c_str(), gproj_id);
        if (_key < 0)
        { 
            perror("ftok");
            return false;
        }
        // degug
        printf("创建键值成功，key:0x%x\n", _key);

        // 使用shmget 创建共享内存
        // shmget 的第一个参数为键值，通过ftok 获取；第二个参数为所创建共享内存的大小；第三个参数为权限标志IPC_CREAT|IPC_EXCL
        // shmget 成功返回 shmid ,失败返回-1
        _shmid = shmget(_key, _size, flags); // 跟文件权限一摸一样
        if (_shmid < 0)
        {
            perror("shmget");
            return false;
        }
        // debug
        printf("sheget 创建共享内存成功， shmid: %d\n", _shmid);
        return true;
    }

public:
    SharedMemory(int size = gdefault_size) 
    : _size(size), _key(0), _shmid(-1), _start_addr(nullptr)
    ,_num(nullptr),_datastart(nullptr)
    ,_windex(0),_rindex(0)
    {}

    // 创建共享内存
    bool Create()
    {
        return ShmHelper(IPC_CREAT | IPC_EXCL | 0666);
    }
    bool Get()//客户不用创建，直接获取
    {
        return ShmHelper(IPC_CREAT);
    }

    // 想要使用共享内存，需要将共享内存映射到当前进程所对应的虚拟地址空间中，即挂载，使用系统调用 shmat,即share memory attach
    bool Attach()
    {
        // shmat 的第一个参数为共享内存标识符shmid , 第二个参数表示该共享内存将要挂载到虚拟地址空间的那个地址处，传NULL可以让OS自动来帮我们指明
        // 第三个参数表示挂接时所要用的选项，传0使用缺省便可;返回值为void* ，失败则是-1，成功则为所挂载数据段的起始虚拟地址
        _start_addr = shmat(_shmid, nullptr, 0); // 要先为共享内存设置权限，再挂载；且其第三个个参数默认挂载的权限使用的就是在shmget 中设置的权限
        if ((long long)_start_addr == -1)        // 此处的操作系统为 x64 , 占8byte
        {
            perror("shmat");
            return false;
        }

        //共享内存的前4字节空间用来存储共享内存中的数据个数
        _num = (int*)_start_addr;
        _datastart = (char*)_start_addr + sizeof(int);
        *_num = 0;

        return true;
    }

    //写入数据
    void AddChar(char ch)
    {
        if(*_num == _size) return;

        ((char*)_datastart)[_windex++] = ch;
        ((char*)_datastart)[_windex] = '\0';

        _windex%=_size;
        (*_num)++;//注意此处的运算符优先级问题
    }

    void PopChar(char* ch)
    {
        if(*_num==0) return;
        *ch = ((char*)_datastart)[_rindex++];
        _rindex %= _size;
        (*_num)--;//注意运算符优先级问题
    }

    //OS在用户层实现了一个数据结构 shmid_ds 可以来获取共享内存的属性，其中shmctl 中的第二个参数设置为IPC_STAT 就可以获取
    void PrintAttr()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid , IPC_STAT , &ds);
        if(n < 0)
        {
            perror("shmctl");
            return;
        }

        //打印struct shmid_ds 中的数据， __key 放在其中的struct ipc_perm shm_perm 中
        printf("key : 0x%x\n" , ds.shm_perm.__key);
        printf("size : %ld\n" , ds.shm_segsz);
        printf("stime : %lu\n" , ds.shm_atime);
        printf("nattach : %ld\n" , ds.shm_nattch);
        
    }
    //去关联
    bool Detach()
    {
        //使用shmdt , 参数为共享内存映射在虚拟地址空间的起始地址，即_start_addr , 为 shmat 的返回值
        int n = shmdt(_start_addr);
        if(n < 0)
        {
            perror("shmdt");
            return false;
        }
        //debug
        printf("将指定的共享内存从进程的地址空间中去关联\n");
        return true;
    }

    // 自动释放所创建的共享内存
    bool RemoveShm()
    {
        // 会使用到系统调用 shmctrl
        // 第一个参数为共享内存的标识符shmid , 第二个参数为会对该共享内存做什么操作，第三个参数为一个结构体，传nullptr 就好
        // 第二个参数传，IPC_RMID；返回值：成功返回0，失败返回-1
        int n = shmctl(_shmid, IPC_RMID, nullptr);
        if (n < 0)
        {
            perror("shmctl");
            return false;
        }
        // debug
        printf("移除共享内存成功\n");
        return true;
    }

    ~SharedMemory()
    {}

private:
    key_t _key;        // 保存共享内存的键值
    int _size;         // 共享内存的大小
    int _shmid;        // 共享内存的标识符
    void *_start_addr; // 所映射到虚拟空间中的起始地址

    //通信
    int * _num;//记录共享内存中的数据个数
    char * _datastart;//存储数据的起始地址
    int _windex;//写
    int _rindex;//读
};

#endif